# python-resmon - Python tools for Resmon monitoring and the Circonus API
# Copyright (c) 2010 Crown coypright
# 
# This file is part of python-resmon.
# 
# python-resmon is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# python-resmon is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with python-resmon.  If not, see <http://www.gnu.org/licenses/>.

import re
import time
import urllib2
import socket
from cStringIO import StringIO

from lxml import etree


from ruleparse.loader import load

from alert import *


class ResmonError(Exception):
    """There was an error connecting to Resmon"""


# Check that results were generated within this timeout
STALE_TIMEOUT = 300

class Stale(object):
    """Signifies that a result was stale and is thus unusable."""

    def __init__(self, age):
        self.age = age


class ResmonBroker(object):
    def get_resmon_url(self, host):
        return 'http://%s:81/' % host.hostname

    def load_metrics(self, url):
        try:
            resp = urllib2.urlopen(url, timeout=20)
        except IOError, e:
            raise ResmonError("Failed to connect to resmon.")

        try:
            doc = etree.parse(resp)
        except socket.timeout:
            raise ResmonError("Timed out reading results from resmon.")
        except IOError, e:
            raise ResmonError("Error reading results from resmon: %s" % e)
        except etree.XMLSyntaxError, e:
            raise ResmonError("Error parsing resmon results: %s" % e)

        now = time.time()
        results = {}
        for check in doc.findall('.//ResmonResult'):
            module = check.get('module')
            service = check.get('service')
            try:
                update = int(check.find('./last_update').text)
            except:
                raise ResmonError("Unable to determine time of last update.")
            else:
                age = int(now - update)

            metrics = []
            for metric in check.findall('.//metric'):
                name = '%s`%s`%s' % (module, service, metric.get('name'))
                if age > STALE_TIMEOUT:
                    results[name] = Stale(age)
                else:
                    results[name] = self.convert_value(metric.text)
        if not results:
            raise ResmonError("Received no results from Resmon")
        return results

    def convert_value(self, value):
        """Autodetect the type of a value and cast to that type."""
        try:
            if re.match(r'^\d+\.\d+', value):
                return float(value)
            elif re.match(r'\d+', value):
                return int(value)
        except (TypeError, ValueError):
            pass
        return value

    def get_metrics(self, host):
        url = self.get_resmon_url(host)
        return self.load_metrics(url)
        


class ResmonChecker(object):
    def __init__(self, rulefile='rules.cm', broker=ResmonBroker()):
        self.model = load(rulefile)
        self.broker = broker

    def run_checks(self):
        for host in self.model:
            for alert in self.check_host(host):
                yield alert

    def check_host(self, host):
        metrics = host.get_metrics()

        try:
            values = self.broker.get_metrics(host)
        except ResmonError, e:
            yield ResmonErrorAlert(host, e.args[0])
            return

        for metric in metrics:
            val = values.get(metric.name, None)

            if val is None:
                yield AbsentAlert(host, metric)
                continue

            if isinstance(val, Stale):
                yield StaleAlert(host, metric, val.age)
                continue

            for rule in metric.alerts:
                criteria = rule.circonus_rule()['criteria']
                rvalue = rule.value
                severity = rule.severity
                if criteria == 'does not match':
                    if str(val) != str(rvalue):
                        yield RuleAlert(severity, host, metric, val)
                        break
                elif criteria == 'match':
                    if str(val) == str(rvalue):
                        yield RuleAlert(severity, host, metric, val)
                        break
                elif criteria == 'max value':
                    if float(val) > float(rvalue):
                        yield RuleAlert(severity, host, metric, val)
                        break
                elif criteria == 'min value':
                    if float(val) < float(rvalue):
                        yield RuleAlert(severity, host, metric, val)
                        break
                else:
                    print "Unknown metric type"


import threading
from Queue import Queue, Empty

class CheckThread(threading.Thread):
    def __init__(self, checker, hostqueue, outqueue):
        super(CheckThread, self).__init__()
        self.daemon = True
        self.checker = checker
        self.hostqueue = hostqueue
        self.outqueue = outqueue
        self.keeprunning = True
        self.start()

    def stop(self):
        self.keeprunning = False

    def run(self):
        while self.keeprunning:
            try:
                host = self.hostqueue.get_nowait()
            except Empty:
                break
            for alert in self.checker.check_host(host):
                self.outqueue.put(alert)


class ParallelResmonChecker(ResmonChecker):
    """Run checks on several hosts simulatenously."""

    WORKERS = 8

    def run_checks(self):
        hostqueue = Queue()
        for host in self.model:
            hostqueue.put(host)
        outqueue = Queue()
        try:
            pool = [CheckThread(self, hostqueue, outqueue) for i in range(self.WORKERS)]

            while True:
                try:
                    yield outqueue.get(True, 2)
                except Empty:
                    if not any([worker.isAlive() for worker in pool]):
                        break
        except (KeyboardInterrupt, SystemExit):
            for worker in pool:
                worker.stop()
            raise
